﻿using System;

namespace Notoric.Model.Utils
{
    /// <summary>
    /// Groups several useful functions for working with symbol durations.
    /// </summary>
    public static class Duration
    {
        /// <summary>
        /// Returns the number of augmentation dots that correspond to a given duration.
        /// </summary>
        /// <param name="duration">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The duration being decomposed.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The number of augmentation dots that corresponds to the given duration.
        /// </para>
        /// </returns>
        public static byte AugmentationDotsCount(ushort duration)
        {
            byte dots = 0;

            if (duration <= Model.Elements.DurationalSymbolElement.Whole)
            {
                var bitMask = Highest2Pow(duration) >> 1;
                while ((duration & bitMask) != 0)
                {
                    bitMask >>= 1;
                    ++dots;
                }
            }

            return dots;
        }

        /// <summary>
        /// Returns the basic duration part (without augmentation dots) from a given duration.
        /// </summary>
        /// <param name="duration">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The duration being decomposed.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The extracted basic duration part.
        /// </para>
        /// </returns>
        public static ushort BasicDuration(ushort duration)
        {
            return Highest2Pow(duration);
        }

        /// <summary>
        /// Returns the order of the highest set bit in a given value.
        /// </summary>
        /// <param name="value">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// The value.
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The order of the highest set bit (first bit has order 0).
        /// </para>
        /// </returns>
        public static byte HighestBit(ushort value)
        {
            if (value == 0)
                return 0;

            byte order = 15;
            while ((value & 0x8000) == 0)
            {
                value <<= 1;
                --order;
            }

            return order;
        }

        /// <summary>
        /// Returns the value that corresponds to the highest set bit in a given value.
        /// </summary>
        /// <param name="value">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The value.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The value (power of two) that corresponds to the highest set bit.
        /// </para>
        /// </returns>
        public static ushort Highest2Pow(ushort value)
        {
            if (value == 0)
                return 0;

            ushort mask = 0x8000;
            while ((value & mask) == 0)
                mask >>= 1;

            return mask;
        }

        /// <summary>
        /// Returns the order of the lowest set bit in a given value.
        /// </summary>
        /// <param name="value">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// The value.
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The order of the lowest set bit (first bit has order 0).
        /// </para>
        /// </returns>
        public static byte LowestBit(ushort value)
        {
            if (value == 0)
                return 0;

            byte order = 0;
            while ((value & 1) == 0)
            {
                value >>= 1;
                ++order;
            }

            return order;
        }

        /// <summary>
        /// Returns the value that corresponds to the lowest set bit in a given value.
        /// </summary>
        /// <param name="value">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The value.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The value (power of two) that corresponds to the lowest set bit.
        /// </para>
        /// </returns>
        public static ushort Lowest2Pow(ushort value)
        {
            if (value == 0)
                return 0;

            ushort mask = 1;
            while ((value & mask) == 0)
                mask <<= 1;

            return mask;
        }

        /// <summary>
        /// Gets the total duration.
        /// </summary>
        /// <param name="basicDuration">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// Basic duration.
        /// </para>
        /// </param>
        /// <param name="augmentationDots">
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The number of augmentation dots.
        /// </para>
        /// </param>
        /// <returns></returns>
        public static ushort TotalDuration(ushort basicDuration, byte augmentationDots)
        {
            ushort increment = (ushort)(basicDuration / 2);
            ushort duration = basicDuration;

            for (int i = 0; i < augmentationDots; ++i)
            {
                duration += increment;
                increment /= 2;
            }

            return duration;
        }
    }
}
